#!/usr/bin/env node

// 预约服务并发压测脚本
// Windows PowerShell 示例：
//   node app/tools/stress-appointment-service.mjs --mode mix --concurrency 16 --duration 60
//   node app/tools/stress-appointment-service.mjs --mode create --concurrency 8 --iterations 500

import crypto from 'node:crypto'
import process from 'node:process'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
// 为压测进程启用并发友好设置（不依赖主进程初始化）
async function setupPragma() {
  try {
    await prisma.$executeRawUnsafe('PRAGMA journal_mode=WAL;')
    await prisma.$executeRawUnsafe('PRAGMA synchronous=NORMAL;')
    await prisma.$executeRawUnsafe('PRAGMA busy_timeout=15000;')
    await prisma.$executeRawUnsafe('PRAGMA temp_store=MEMORY;')
  }
  catch {}
}

function parseArgs() {
  const argv = process.argv.slice(2)
  const args = Object.create(null)
  for (let i = 0; i < argv.length; i++) {
    const k = argv[i]
    const v = argv[i + 1]
    if (!k.startsWith('--'))
      continue
    switch (k) {
      case '--mode': args.mode = v; i++; break
      case '--concurrency': args.concurrency = Number(v || 0) || 8; i++; break
      case '--duration': args.duration = Number(v || 0) || 0; i++; break // 秒
      case '--iterations': args.iterations = Number(v || 0) || 0; i++; break
      case '--seed': args.seed = Number(v || 0) || 200; i++; break
      case '--office': args.office = v || 'HKLO'; i++; break
      default: break
    }
  }
  if (!['create', 'update', 'book', 'mix'].includes(args.mode))
    args.mode = 'mix'
  if (!args.concurrency || args.concurrency < 1)
    args.concurrency = 8
  if (!args.duration && !args.iterations)
    args.duration = 30
  if (args.duration && args.iterations)
    args.iterations = 0
  return args
}

function randomInt(maxExclusive) { return Math.floor(Math.random() * maxExclusive) }

function genPassportNumber() {
  // 简化：首位 E 或 C，随机是否两位前缀，总长9
  const first = Math.random() < 0.6 ? 'E' : 'C'
  const two = Math.random() < 0.5
  const prefix = two ? (first + String.fromCharCode(65 + randomInt(26))) : first
  const digits = two ? 7 : 8
  const tail = String(randomInt(10 ** digits)).padStart(digits, '0')
  return prefix + tail
}

function genLicenseNumber() {
  // 简化中国身份证-like：地区+年份+MM+DD+序列+校验
  const region = String(100 + randomInt(899)) + String(100 + randomInt(899))
  const year = 1970 + randomInt(30)
  const month = String(1 + randomInt(12)).padStart(2, '0')
  const day = String(1 + randomInt(28)).padStart(2, '0')
  const seq = String(1 + randomInt(999)).padStart(3, '0')
  const check = '0123456789X'[randomInt(11)]
  return `${region}${year}${month}${day}${seq}${check}`
}

function genName() {
  const seeds = ['张伟', '王芳', '李秀英', '刘洋', '陈强', '杨静', '赵磊', '黄敏', '周杰', '吴霞', '郑刚', '何丽', '马超', '胡军', '高磊', '郭志', '罗燕', '宋健', '唐宁']
  return seeds[randomInt(seeds.length)]
}

function hrtimeMs() { const [s, ns] = process.hrtime(); return s * 1000 + ns / 1e6 }

function percentile(sorted, p) {
  if (sorted.length === 0)
    return 0
  const idx = Math.ceil((p / 100) * sorted.length) - 1
  return sorted[Math.max(0, Math.min(sorted.length - 1, idx))]
}

async function ensureSeedRecords(count, office) {
  const existing = await prisma.appointment_management.count()
  const need = Math.max(0, count - existing)
  if (need === 0)
    return
  const batch = []
  for (let i = 0; i < need; i++) {
    batch.push(prisma.appointment_management.create({
      data: {
        sequence_number: String(Date.now()) + String(i).padStart(4, '0'),
        passport_number: genPassportNumber(),
        license_number: genLicenseNumber(),
        full_name: genName(),
        email: `${crypto.randomBytes(4).toString('hex')}@example.com`,
        issuing_country: 'CN',
        issuing_authority: 'GD',
        service_type: 'DI',
        priority: 1,
        remarks: 'seed',
        is_placeholder: true,
        office_code: office,
      },
    }))
  }
  // 分批提交，避免一次性过大
  const chunk = 100
  for (let i = 0; i < batch.length; i += chunk) {
    await Promise.all(batch.slice(i, i + chunk))
  }
}

async function randomExistingId() {
  const rows = await prisma.appointment_management.findMany({
    select: { id: true },
    take: 100,
    orderBy: { updated_at: 'desc' },
  })
  if (rows.length === 0)
    return null
  return rows[randomInt(rows.length)].id
}

async function opCreate(office) {
  try {
    const t0 = hrtimeMs()
    await prisma.appointment_management.create({
      data: {
        sequence_number: String(Date.now()) + crypto.randomBytes(2).toString('hex'),
        passport_number: genPassportNumber(),
        license_number: genLicenseNumber(),
        full_name: genName(),
        email: `${crypto.randomBytes(4).toString('hex')}@example.com`,
        issuing_country: 'CN',
        issuing_authority: 'GD',
        service_type: 'DI',
        priority: 1,
        remarks: 'lt:create',
        is_placeholder: true,
        office_code: office,
      },
    })
    return { ok: true, ms: hrtimeMs() - t0 }
  }
  catch (e) {
    return { ok: false, code: e?.code || 'ERR', msg: e?.message || String(e), ms: 0 }
  }
}

async function opUpdate() {
  const id = await randomExistingId()
  if (!id)
    return { ok: false, code: 'EMPTY', msg: 'no-record', ms: 0 }
  try {
    const t0 = hrtimeMs()
    await prisma.appointment_management.update({ where: { id }, data: {
      remarks: `lt:update:${crypto.randomBytes(2).toString('hex')}`,
      priority: 1 + randomInt(5),
    } })
    return { ok: true, ms: hrtimeMs() - t0 }
  }
  catch (e) {
    return { ok: false, code: e?.code || 'ERR', msg: e?.message || String(e), ms: 0 }
  }
}

async function opBook() {
  const id = await randomExistingId()
  if (!id)
    return { ok: false, code: 'EMPTY', msg: 'no-record', ms: 0 }
  try {
    const t0 = hrtimeMs()
    await prisma.appointment_management.update({ where: { id }, data: {
      appointment_status: 'booked',
      reference_number: `RF${crypto.randomBytes(3).toString('hex').toUpperCase()}`,
      appointment_date: `${String(10 + randomInt(10)).padStart(2, '0')}-12-2025`,
      appointment_time: '09:00-10:00',
    } })
    return { ok: true, ms: hrtimeMs() - t0 }
  }
  catch (e) {
    return { ok: false, code: e?.code || 'ERR', msg: e?.message || String(e), ms: 0 }
  }
}

async function run() {
  const { mode, concurrency, duration, iterations, seed, office } = parseArgs()
  console.log(`[INFO] mode=${mode} concurrency=${concurrency} duration=${duration}s iterations=${iterations} seed=${seed}`)

  await ensureSeedRecords(seed, office || 'HKLO')

  const stopAt = duration ? Date.now() + duration * 1000 : 0
  const targetIters = iterations || 0

  const latencies = []
  const errors = Object.create(null)
  let total = 0
  let success = 0

  function pickOp() {
    if (mode === 'create')
      return () => opCreate(office)
    if (mode === 'update')
      return opUpdate
    if (mode === 'book')
      return opBook
    const r = Math.random()
    if (r < 0.34)
      return () => opCreate(office)
    if (r < 0.67)
      return opUpdate
    return opBook
  }

  async function worker(_idx) {
    let i = 0
    while (true) {
      if (stopAt && Date.now() >= stopAt)
        break
      if (targetIters && i >= targetIters)
        break
      i++
      total++
      const op = pickOp()
      const res = await op()
      if (res.ok) {
        success++
        if (res.ms && Number.isFinite(res.ms))
          latencies.push(res.ms)
      }
      else {
        const key = String(res.code || 'ERR')
        errors[key] = (errors[key] || 0) + 1
      }
    }
  }

  const workers = []
  for (let i = 0; i < concurrency; i++) workers.push(worker(i))
  const tStart = Date.now()
  await setupPragma()
  await Promise.all(workers)
  const elapsedSec = (Date.now() - tStart) / 1000

  latencies.sort((a, b) => a - b)
  const p50 = percentile(latencies, 50)
  const p95 = percentile(latencies, 95)
  const p99 = percentile(latencies, 99)
  const tps = elapsedSec > 0 ? (success / elapsedSec) : success

  console.log('\n=== 并发压测结果 ===')
  console.log(`总操作: ${total}`)
  console.log(`成功数: ${success}`)
  console.log(`失败数: ${total - success}`)
  console.log(`耗时(秒): ${elapsedSec.toFixed(2)}`)
  console.log(`吞吐(TPS): ${tps.toFixed(2)}`)
  console.log(`延迟(ms): p50=${p50.toFixed(1)} p95=${p95.toFixed(1)} p99=${p99.toFixed(1)}`)
  if (Object.keys(errors).length > 0) {
    console.log('错误分布:')
    for (const k of Object.keys(errors)) console.log(`  ${k}: ${errors[k]}`)
  }
}

run()
  .catch((e) => {
    console.error('[ERROR]', e?.message || e)
    process.exit(1)
  })
  .finally(async () => {
    try { await prisma.$disconnect() }
    catch {}
  })
